Skip to content

filetransfer: handle exceptions thrown from enqueueItem#348

Open
cole-h wants to merge 3 commits intomainfrom
filetransfer-enqueue-exceptions
Open

filetransfer: handle exceptions thrown from enqueueItem#348
cole-h wants to merge 3 commits intomainfrom
filetransfer-enqueue-exceptions

Conversation

@cole-h
Copy link
Member

@cole-h cole-h commented Feb 12, 2026

Motivation

Context

Summary by CodeRabbit

  • Bug Fixes
    • Enqueue operations for file transfers are now non-throwing and convert failures into per-transfer failure states, improving reliability and preserving async behavior.
  • Refactor
    • Internal transfer handle behavior adjusted to improve stability when creating, enqueueing, and unpausing transfers, reducing unexpected error propagation.

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Added noexcept to FileTransfer::enqueueFileTransfer declarations and overrides; updated enqueue implementations to build local TransferItem(s), catch nix::Error on enqueue failures, mark items failed, and return ItemHandle without propagating exceptions.

Changes

Cohort / File(s) Summary
Header API declarations
src/libstore/include/nix/store/filetransfer.hh
Changed ItemHandle to hold ref<Item> and updated its constructor. Added noexcept to both virtual enqueueFileTransfer(...) (callback) and the future-returning enqueueFileTransfer(...) declaration.
Implementation with error handling
src/libstore/filetransfer.cc
Applied noexcept to implementations, construct local TransferItem/TransferItemS3, wrap enqueueItem calls in try-catch for nix::Error, mark failed items on exception, and return handles that wrap moved ref<Item> instead of propagating exceptions. Also adjusted unpauseTransfer to dereference via *handle.item.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I built a tiny transfer nest, so calm and neat,
No throws to startle — just a steady beat.
If enqueue slips, I tuck it, mark it done,
Hand back its handle, hop off for fun. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: adding exception handling to enqueueItem in the filetransfer component, as evidenced by the introduction of try-catch blocks and noexcept qualifiers throughout the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch filetransfer-enqueue-exceptions

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

modifiedRequest.setupForS3();
return enqueueItem(make_ref<TransferItem>(*this, std::move(modifiedRequest), std::move(callback)));
auto item = make_ref<TransferItem>(*this, std::move(modifiedRequest), std::move(callback));
try {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was suggested that I move this try-catch into enqueueItem directly, but I decided not to do that because I didn't want to mess with the fix in NixOS#14418

I tried a few different ways to reduce the duplication, but I couldn't figure out the best way to do that. ref<TransferItem> item; outside of the if doesn't work because there's no constructor, and I didn't want to copy request every time in the off chance that it's an s3 request...

Happy to take suggestions on improvements here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my comment below (I should have posted it here). Refactoring is definitely doable if item is initialized once and unconditionally, meaning that its value should come from another function (make it inline if you want). No new copying is needed, the request is passed by reference and the callback is moved.

@cole-h cole-h requested a review from edolstra February 12, 2026 17:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/libstore/filetransfer.cc`:
- Around line 968-989: The failure path in enqueueFileTransfer can return an
ItemHandle that refers to a destroyed TransferItem when enqueueItem throws;
update the failure return to transfer ownership so the returned ItemHandle owns
a ref to the item (e.g., change ItemHandle to hold a ref<Item> or add/use an
ItemHandle constructor that accepts the ref), and in both branches (the s3
modifiedRequest branch and the normal branch around enqueueItem)
construct/return the ItemHandle from the owning ref (move the ref from the local
variable into the ItemHandle) instead of creating an ItemHandle from a raw Item
reference; ensure item->fail(e) is called while the ref is still held so the
item remains alive for callers (e.g., unpause) after the function returns.

@github-actions
Copy link

github-actions bot commented Feb 12, 2026

@github-actions github-actions bot temporarily deployed to pull request February 12, 2026 17:14 Inactive
@cole-h cole-h force-pushed the filetransfer-enqueue-exceptions branch from a6402da to 2208cf9 Compare February 12, 2026 17:18
@github-actions github-actions bot temporarily deployed to pull request February 12, 2026 17:26 Inactive
} catch (const nix::Error & e) {
item->fail(e);
return ItemHandle(static_cast<Item &>(*item));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a lot of repetition. Maybe this function should be refactored?

    inline ref<TransferItem>
    makeTransferItem(const FileTransferRequest & request, Callback<FileTransferResult> callback) noexcept
    {
        /* Handle s3:// URIs by converting to HTTPS and optionally adding auth */
        if (request.uri.scheme() == "s3") {
            auto modifiedRequest = request;
            modifiedRequest.setupForS3();
            return make_ref<TransferItem>(*this, std::move(modifiedRequest), std::move(callback));
        } else {
            return make_ref<TransferItem>(*this, request, std::move(callback));
        }
    }

    ItemHandle
    enqueueFileTransfer(const FileTransferRequest & request, Callback<FileTransferResult> callback) noexcept override
    {
        const auto item = makeTransferItem(request, std::move(callback));

        try {
            return enqueueItem(item);
        } catch (const nix::Error & e) {
            item->fail(e);
            return ItemHandle(static_cast<Item &>(*item));
        }
    }

It compiles, but please don't trust my choices about naming and inlining too much.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/libstore/filetransfer.cc (1)

1063-1074: ⚠️ Potential issue | 🟠 Major

Same noexcept concern applies here.

std::make_shared<std::promise<...>>() can throw std::bad_alloc, but the function is noexcept. If memory allocation fails, std::terminate will be called.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libstore/filetransfer.cc` around lines 1063 - 1074, The function
FileTransfer::enqueueFileTransfer is marked noexcept but performs a potentially
throwing allocation via std::make_shared<std::promise<FileTransferResult>>;
remove the noexcept specifier from FileTransfer::enqueueFileTransfer (or
otherwise ensure allocation cannot throw, e.g., use a preallocated promise or
not allocate) so allocation failures do not call std::terminate; update the
signature of enqueueFileTransfer and any callers if necessary to propagate
exceptions normally when std::make_shared<std::promise<FileTransferResult>>
throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/libstore/filetransfer.cc`:
- Around line 968-991: The function enqueueFileTransfer is marked noexcept but
operations like modifiedRequest.setupForS3(), make_ref<TransferItem>(...), and
the TransferItem constructor can throw, which would call std::terminate; to fix,
remove the noexcept specifier or wrap the entire function body in a try-catch
that catches std::exception (and ...), and in the catch create and return a
failed ItemHandle (or otherwise return an ItemHandle that signals failure) while
calling item->fail(e) when a TransferItem exists; specifically adjust the s3
branch (where setupForS3() is called) and the common path that constructs
make_ref<TransferItem> so exceptions are caught before escaping
enqueueFileTransfer and handled by returning a dummy/failed ItemHandle or
propagating safely if you choose to drop noexcept.

---

Outside diff comments:
In `@src/libstore/filetransfer.cc`:
- Around line 1063-1074: The function FileTransfer::enqueueFileTransfer is
marked noexcept but performs a potentially throwing allocation via
std::make_shared<std::promise<FileTransferResult>>; remove the noexcept
specifier from FileTransfer::enqueueFileTransfer (or otherwise ensure allocation
cannot throw, e.g., use a preallocated promise or not allocate) so allocation
failures do not call std::terminate; update the signature of enqueueFileTransfer
and any callers if necessary to propagate exceptions normally when
std::make_shared<std::promise<FileTransferResult>> throws.

Comment on lines +968 to 991
ItemHandle
enqueueFileTransfer(const FileTransferRequest & request, Callback<FileTransferResult> callback) noexcept override
{
/* Handle s3:// URIs by converting to HTTPS and optionally adding auth */
if (request.uri.scheme() == "s3") {
auto modifiedRequest = request;
modifiedRequest.setupForS3();
return enqueueItem(make_ref<TransferItem>(*this, std::move(modifiedRequest), std::move(callback)));
auto item = make_ref<TransferItem>(*this, std::move(modifiedRequest), std::move(callback));
try {
return enqueueItem(item);
} catch (const nix::Error & e) {
item->fail(e);
return ItemHandle(ref<Item>(std::move(item)));
}
}

return enqueueItem(make_ref<TransferItem>(*this, request, std::move(callback)));
auto item = make_ref<TransferItem>(*this, request, std::move(callback));
try {
return enqueueItem(item);
} catch (const nix::Error & e) {
item->fail(e);
return ItemHandle(ref<Item>(std::move(item)));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual function in the file to confirm its current state
head -1000 src/libstore/filetransfer.cc | tail -100

Repository: DeterminateSystems/nix-src

Length of output: 3430


🏁 Script executed:

# Check if setupForS3 can throw nix::Error
rg -n 'setupForS3' --type cpp -B 2 -A 10

Repository: DeterminateSystems/nix-src

Length of output: 6673


🏁 Script executed:

# Check for throw statements in s3-related code
rg -n 'throw' --type cpp -g '*s3*' | head -30

Repository: DeterminateSystems/nix-src

Length of output: 1282


🏁 Script executed:

# Look for ParsedS3URL::parse implementation
rg -n 'ParsedS3URL::parse' --type cpp -B 2 -A 15

Repository: DeterminateSystems/nix-src

Length of output: 7084


🏁 Script executed:

# Check if TransferItem constructor can throw
rg -n 'struct TransferItem\|class TransferItem' --type cpp -A 20

Repository: DeterminateSystems/nix-src

Length of output: 52


noexcept may cause std::terminate if pre-enqueue operations throw.

The function is marked noexcept, but several operations before the try-catch blocks can throw:

  • modifiedRequest.setupForS3() (line 974) calls ParsedS3URL::parse() which throws BadURL (e.g., for invalid bucket names or missing keys)
  • make_ref<TransferItem>(...) (lines 975, 984) can throw std::bad_alloc or exceptions from the TransferItem constructor

If any of these throw, std::terminate will be called. Consider wrapping the entire function body in a try-catch, or removing noexcept if the exception guarantee cannot be upheld.

🛠️ Suggested fix: wrap entire body in try-catch
 ItemHandle
 enqueueFileTransfer(const FileTransferRequest & request, Callback<FileTransferResult> callback) noexcept override
 {
+    try {
     /* Handle s3:// URIs by converting to HTTPS and optionally adding auth */
     if (request.uri.scheme() == "s3") {
         auto modifiedRequest = request;
         modifiedRequest.setupForS3();
         auto item = make_ref<TransferItem>(*this, std::move(modifiedRequest), std::move(callback));
-        try {
-            return enqueueItem(item);
-        } catch (const nix::Error & e) {
-            item->fail(e);
-            return ItemHandle(ref<Item>(std::move(item)));
-        }
+        return enqueueItem(item);
     }

     auto item = make_ref<TransferItem>(*this, request, std::move(callback));
-    try {
-        return enqueueItem(item);
-    } catch (const nix::Error & e) {
-        item->fail(e);
-        return ItemHandle(ref<Item>(std::move(item)));
+    return enqueueItem(item);
+    } catch (const nix::Error & e) {
+        // Need to handle the case where item doesn't exist yet
+        // This requires restructuring to ensure item is always created first
+        // or using a different error reporting mechanism
     }
 }

Note: A complete fix requires restructuring to handle exceptions that occur before the TransferItem is created (e.g., in setupForS3()), since there's no item to mark as failed in that case. You may need to create a "dummy" failed item or reconsider the noexcept guarantee.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libstore/filetransfer.cc` around lines 968 - 991, The function
enqueueFileTransfer is marked noexcept but operations like
modifiedRequest.setupForS3(), make_ref<TransferItem>(...), and the TransferItem
constructor can throw, which would call std::terminate; to fix, remove the
noexcept specifier or wrap the entire function body in a try-catch that catches
std::exception (and ...), and in the catch create and return a failed ItemHandle
(or otherwise return an ItemHandle that signals failure) while calling
item->fail(e) when a TransferItem exists; specifically adjust the s3 branch
(where setupForS3() is called) and the common path that constructs
make_ref<TransferItem> so exceptions are caught before escaping
enqueueFileTransfer and handled by returning a dummy/failed ItemHandle or
propagating safely if you choose to drop noexcept.

@github-actions github-actions bot temporarily deployed to pull request February 20, 2026 18:50 Inactive
@xokdvium
Copy link

I've actually explored the approach with making the thing noexcept, but I think the double-callback issue was fully addressed with NixOS#14838.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants